package net.fornwall.eclipsecoder.arena;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

import net.fornwall.eclipsecoder.actions.SubmitAction;
import net.fornwall.eclipsecoder.languages.LanguageSupport;
import net.fornwall.eclipsecoder.languages.LanguageSupportFactory;
import net.fornwall.eclipsecoder.preferences.EclipseCoderPlugin;
import net.fornwall.eclipsecoder.stats.ProblemStatement;
import net.fornwall.eclipsecoder.util.Utilities;

import org.eclipse.ui.PlatformUI;

import com.topcoder.client.contestApplet.frames.CodingFrame;
import com.topcoder.client.contestant.ProblemComponentModel;
import com.topcoder.shared.language.CPPLanguage;
import com.topcoder.shared.language.JavaLanguage;
import com.topcoder.shared.language.Language;
import com.topcoder.shared.problem.DataType;
import com.topcoder.shared.problem.Renderer;
import com.topcoder.shared.problem.TestCase;

public class EclipseCoderEntryPoint extends EntryPoint {
	private static EclipseCoderEntryPoint instance;

	// TODO: Only used by SubmitAction - remove and give LanguageSupport to
	// SubmitAction?
	// Perhaps have a class method submit() which uses the instance...
	public static EclipseCoderEntryPoint getInstance() {
		return instance;
	}

	CodingFrame codingFrame;

	DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

	/**
	 * The JPanel given to the ContestApplet in getEditorPanel(). It is the
	 * panel that is displayed inside the CodingFrame.
	 */
	private JPanel editorPanel = new JPanel(new GridBagLayout());

	LanguageSupport languageSupport;

	JTextArea logArea = new JTextArea();

	private JTextField pointsField = new JTextField();

	private PointsUpdater pointsUpdater;

	private Timer setSourceTimer = new Timer(500, new ActionListener() {
		public void actionPerformed(ActionEvent e) {
			doSendProblem();
		}
	}) {
		private static final long serialVersionUID = 1L;

		{
			setRepeats(false);
		}
	};

	/**
	 * Due to the isCacheable() method returning true, only one instance of this
	 * class should ever be created by the TopCoder contest applet.
	 */
	public EclipseCoderEntryPoint() {
		EclipseCoderEntryPoint.instance = this;

		editorPanel.setForeground(Color.WHITE);
		editorPanel.setBackground(Color.BLACK);

		GridBagConstraints gc = new GridBagConstraints();
		gc.gridx = gc.gridy = 0;
		gc.gridwidth = 2;
		gc.fill = GridBagConstraints.BOTH;
		gc.weightx = gc.weighty = 1.0;

		logArea.setEditable(false);
		logArea.setForeground(Color.WHITE);
		logArea.setBackground(Color.BLACK);
		editorPanel.add(new JScrollPane(logArea), gc);

		gc.gridwidth = 1;
		gc.gridy++;
		gc.weighty = 0.0;
		gc.weightx = 0.0;
		gc.insets.bottom = gc.insets.top = 4;
		JLabel pointsLabel = new JLabel("Points:");
		pointsLabel.setForeground(Color.WHITE);
		pointsLabel.setBackground(Color.BLACK);
		editorPanel.add(pointsLabel, gc);

		gc.insets.left = 4;
		gc.gridx++;
		gc.weightx = 1.0;
		pointsField.setForeground(Color.WHITE);
		pointsField.setBackground(Color.BLACK);
		pointsField.setEditable(false);
		editorPanel.add(pointsField, gc);

		pointsUpdater = new PointsUpdater(pointsField);
	}

	public void appendLogMessage(final String message) {
		Runnable runnable = new Runnable() {
			public void run() {
				logArea.append(dateFormat.format(new Date()) + " " + message
						+ "\n");
			}
		};
		if (SwingUtilities.isEventDispatchThread()) {
			runnable.run();
		} else {
			SwingUtilities.invokeLater(runnable);
		}
	}

	void doSendProblem() {
		PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
			public void run() {
				try {
					languageSupport.createProject(problemStatement);
				} catch (Exception e) {
					showStackTrace(e);
				}
			}
		});
	}

	/**
	 * Find the CodingFrame containing the "Compile" and "Submit" buttons.
	 */
	private void findCodingFrame() {
		Component component = editorPanel;

		while ((component = component.getParent()) != null) {
			if (component instanceof Container) {
				Component[] subComponents = ((Container) component)
						.getComponents();
				for (int i = 0; i < subComponents.length; i++) {
					if (subComponents[i] instanceof Container) {
						if (component instanceof CodingFrame) {
							codingFrame = (CodingFrame) component;
						}
					}
				}
			}
		}

		if (codingFrame == null) {
			showMessage("Critical error: The CodingFrame could not be found and EclipseCoders functionality will "
					+ "be reduced. You will have to manually compile and submit your solution after sending them "
					+ "from inside Eclipse");
		}

	}

	/**
	 * @see net.fornwall.eclipsecoder.arena.EntryPoint#getEditorPanel()
	 */
	@Override
	public JPanel getEditorPanel() {
		return editorPanel;
	}

	@Override
	public String getSource() {
		if (languageSupport == null) {
			return null;
		}

		String source = null;
		try {
			source = languageSupport.getSubmissionOutsideThread();
		} catch (Exception e) {
			showStackTrace(e);
		}
		return source;
	}

	@Override
	public boolean isCacheable() {
		return true;
	}

	void showStackTrace(Exception e) {
		e.printStackTrace();
		appendLogMessage(Utilities.getStackTrace(e));
	}

	/**
	 * Programmatically press the "Compile" button.
	 */
	void pressCompileButton() {
		if (!SwingUtilities.isEventDispatchThread()) {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					try {
						pressCompileButton();
					} catch (Exception e) {
						showStackTrace(e);
					}
				}
			});
			return;
		}

		if (codingFrame == null) {
			showMessage("pressCompileButton() called, but codingFrame is null!");
			return;
		}

		try {
			Method m = CodingFrame.class.getDeclaredMethod(
					"compileButtonEvent", new Class[0]);
			m.setAccessible(true);
			m.invoke(codingFrame, new Object[0]);
		} catch (Exception e) {
			showStackTrace(e);
		}
	}

	void pressSubmitButton() {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				try {
					Method m = CodingFrame.class.getDeclaredMethod(
							"submitButtonEvent", new Class[0]);
					m.setAccessible(true);
					m.invoke(codingFrame, new Object[0]);
				} catch (Exception e) {
					showStackTrace(e);
				}
			}
		});
	}

	/**
	 * Transforms a TopCoder problem statement to an EclipseCoder
	 * ProblemStatement object.
	 * 
	 * @param problem
	 *            the TopCoder problem statement
	 * @param renderer
	 *            the TopCoder Renderer
	 * @return the ProblemStatement object
	 * @see ProblemStatement
	 */
	private static ProblemStatement transformToProblemStatement(
			ProblemComponentModel problem, Language language, Renderer renderer)
			throws Exception {
		ProblemStatement result = new ProblemStatement();
		result.setSolutionClassName(problem.getClassName());
		result.setSolutionMethodName(problem.getMethodName());
		Class<?> returnType = toClass(problem.getReturnType());
		result.setReturnType(returnType);

		List<Class<?>> parameterTypes = new ArrayList<Class<?>>();
		for (DataType dataType : problem.getParamTypes()) {
			parameterTypes.add(toClass(dataType));
		}
		result.setParameterTypes(parameterTypes);
		result.setParameterNames(Arrays.asList(problem.getParamNames()));

		List<ProblemStatement.TestCase> testCases = new ArrayList<ProblemStatement.TestCase>();
		for (TestCase testCase : problem.getTestCases()) {
			Object output = ProblemStatement.parseType(returnType, testCase
					.getOutput());
			Object[] input = new Object[testCase.getInput().length];
			for (int i = 0; i < input.length; i++) {
				input[i] = ProblemStatement.parseType(parameterTypes.get(i),
						testCase.getInput()[i]);
			}
			testCases.add(new ProblemStatement.TestCase(output, input));
		}
		result.setTestCases(testCases);

		String html = renderer.toHTML(language);
		if (html.indexOf("<title>") == -1) {
			html = html.replaceFirst("<html>", "<html><head><title>"
					+ result.getClassName() + "</title></head>");
		}
		result.setHtmlDescription(html);

		return result;
	}

	/** Used in the conversion to ProblemStatement */
	private static Class<?> toClass(DataType dataType) throws Exception {
		Map<String, Class<?>> classMap = new HashMap<String, Class<?>>();
		classMap.put("char", Character.class);
		classMap.put("char[]", Character[].class);
		classMap.put("int", Integer.class);
		classMap.put("int[]", Integer[].class);
		classMap.put("long", Long.class);
		classMap.put("long[]", Long[].class);
		classMap.put("double", Double.class);
		classMap.put("double[]", Double[].class);
		classMap.put("String", String.class);
		classMap.put("String[]", String[].class);

		String className = dataType.getDescriptor(JavaLanguage.class
				.newInstance());

		if (classMap.containsKey(className)) {
			return classMap.get(className);
		}

		throw new IllegalArgumentException("Unknown datatype: " + className);
	}

	ProblemStatement problemStatement;

	/**
	 * @see net.fornwall.eclipsecoder.arena.EntryPoint#setProblemComponent(com.topcoder.client.contestant.ProblemComponentModel,
	 *      com.topcoder.shared.language.Language,
	 *      com.topcoder.shared.problem.Renderer)
	 */
	@Override
	public void setProblemComponent(ProblemComponentModel componentModel,
			Language language, Renderer renderer) {
		try {
			appendLogMessage("Opened problem \""
					+ componentModel.getClassName() + "\" in language \""
					+ language.getName() + "\"");
			findCodingFrame();

			String languageName = null;
			if (language instanceof CPPLanguage) {
				languageName = LanguageSupport.CPP_LANGUAGE_NAME;
			} else if (language instanceof JavaLanguage) {
				languageName = LanguageSupport.JAVA_LANGUAGE_NAME;
			} else {
				languageSupport = null;
				appendLogMessage("EclipseCoder does not support the "
						+ language.getName()
						+ " programming language - only Java and C++.\n"
						+ "Change to one of the supported languages if you want to use EclipseCoder or switch to another editor.");
				return;
			}

			pointsUpdater.start(componentModel.getPoints().doubleValue());

			languageSupport = LanguageSupportFactory
					.createLanguageSupport(languageName);
			if (languageSupport == null) {
				appendLogMessage("No plugin support for the "
						+ language.getName()
						+ " programming language found.\n"
						+ "Install the appropriate plugin from the EclipseCoder update site if you want to use EclipseCoder with this language.");
				return;
			}

			problemStatement = transformToProblemStatement(componentModel,
					language, renderer);
		} catch (Exception e) {
			showStackTrace(e);
		}
	}

	/**
	 * @see net.fornwall.eclipsecoder.arena.EntryPoint#setSource(java.lang.String)
	 */
	@Override
	public void setSource(String source) {
		setSourceTimer.stop();
		if (languageSupport == null) {
			return;
		}

		if (source == null || source.length() == 0) {
			// Initial from topcoder - we may get stored solution in a little
			// while..
			languageSupport.setSavedSource(null); // use generated if there is
			// no stored solution
			setSourceTimer.start();
		} else {
			// TODO: Overflow which creates a dialog when source is large -
			// replace
			// with custom dialog
			if (JOptionPane.showConfirmDialog(getEditorPanel(), source,
					"Use the source saved on the TopCoder server?",
					JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
				languageSupport.setSavedSource(source);
			}
			// This is the updated version with source, no need to wait now.
			doSendProblem();
		}

	}

	/**
	 * Utility method for displaying a dialog with a message in the GUI thread.
	 * 
	 * @param message
	 *            the message to be shown
	 */
	private void showMessage(final String message) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				try {
					JOptionPane.showMessageDialog(codingFrame, message);
				} catch (Exception e) {
					showStackTrace(e);
				}
			}
		});
	}

	/**
	 * @see net.fornwall.eclipsecoder.arena.EntryPoint#startUsing()
	 */
	@Override
	public void startUsing() {
		SwingUtilities.updateComponentTreeUI(editorPanel);
		SubmitAction.setEnabled(true);
		if (EclipseCoderPlugin.shouldCloseDialogs()) {
			DialogCloser.start();
		}
	}

	/**
	 * @see net.fornwall.eclipsecoder.arena.EntryPoint#stopUsing()
	 */
	@Override
	public void stopUsing() {
		SubmitAction.setEnabled(false);
		if (EclipseCoderPlugin.shouldCloseDialogs()) {
			DialogCloser.stop();
		}
		pointsUpdater.stop();
	}

	/**
	 * Called from SWT Display thread.
	 */
	public void submit() {
		try {
			pressCompileButton();
		} catch (Exception e) {
			Utilities.showException(e);
		}
	}

}